#region Using Statements
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
#endregion

namespace DirectZZT.Shared.Core
{
    /// <summary>
    /// Caches types in a big library to avoid having multiple classes scan multiple times.
    /// Any class can retrieve types from here to use them in whichever way they want.
    /// </summary>
    public sealed class TypeManager
    {
        #region Constants

        /// <summary>
        /// The default capacity amount.
        /// </summary>
        private const int DefaultAmount = 32;

        #endregion

        #region Singleton (Thread-safe implementation)

        /// <summary>
        /// (Singleton) Gets the Instance. This is thread-safe.
        /// </summary>
        public static TypeManager Instance
        {
            get
            {
                // DoubleLock 
                if (_instance == null)
                {
                    lock (_lock)
                    {
                        if (_instance == null)
                        {
                            _instance = new TypeManager();
                        }
                    }
                }
                return _instance;
            }
        }

        private static object _lock = new object();
        private static volatile TypeManager _instance = null;

        #endregion

        #region Properties

        /// <summary>
        /// Gets if the Type holder is initialized and has scanned all assemblies.
        /// </summary>
        public bool IsInitialized { get; private set; }
        /// <summary>
        /// Gets a list of all types that we have found in our assemblies.
        /// </summary>
        public IList<ExportedType> Exports { get; private set; }

        #endregion

        #region Constructors

        /// <summary>
        /// Initializes a new instance of the TypeManager class.
        /// </summary>
        private TypeManager()
        {
            IsInitialized = false;
            Exports = new List<ExportedType>(DefaultAmount);
        }

        #endregion

        #region Methods

        /// <summary>
        /// Scans all assemblies that are in the list.
        /// </summary>
        /// <param name="assembliesToScan">A list containing all assemblies (full file names) to scan.
        /// If this parameter is <c>null</c>, then all assemblies in the working directory will be scanned.</param>
        public void Initialize(IEnumerable<string> assembliesToScan)
        {
            if (IsInitialized)
            {
                return;
            }

            Stopwatch sw = Stopwatch.StartNew();

            // if there are no desired assemblies then we take all assemblies we can find in the working directory
            if (assembliesToScan == null)
            {
                string workingDirectory = Assembly.GetExecutingAssembly().GetWorkingDirectory();
                List<string> tmp = new List<string>(2);
                // alright, lets scan all assemblies in the working directory
                tmp.AddRange(Directory.GetFiles(workingDirectory, "*.exe", SearchOption.TopDirectoryOnly));
                tmp.AddRange(Directory.GetFiles(workingDirectory, "*.dll", SearchOption.TopDirectoryOnly));
                assembliesToScan = tmp;
            }

            // load and check each assembly's types
            foreach (string file in assembliesToScan)
            {
                Assembly assembly = null;

                try
                {
                    assembly = Assembly.Load(AssemblyName.GetAssemblyName(file));

                    ScanAssembly(assembly);

                }
                catch (FileLoadException fle)
                {
                    // this exception can be ignored here
                    Logging.GetLogging().LogException("Core", fle);
                    Logging.GetLogging().Log(LoggingMessageType.Warning, "Core", "", "Could not load this assembly (see exception details below).");
                    continue;
                }
                catch (ReflectionTypeLoadException rtle)
                {
                    // this exception can be ignored here
                    Logging.GetLogging().LogException("Core", rtle);
                    Logging.GetLogging().LogFormat(LoggingMessageType.Warning, "Core", "", "Skipped assembly '{0}' because it contains some invalid types (see exception details below).", assembly);
                    continue;
                }
                catch (TypeLoadException tle)
                {
                    // this exception can be ignored here
                    Logging.GetLogging().LogException("Core", tle);
                    Logging.GetLogging().LogFormat(LoggingMessageType.Warning, "Core", "", "Skipped assembly '{0}' because it contains some invalid types (see exception details below).", assembly);
                    continue;
                }
                catch (BadImageFormatException bife)
                {
                    // this exception can be ignored here
                    Logging.GetLogging().LogException("Core", bife);
                    Logging.GetLogging().LogFormat(LoggingMessageType.Warning, "Core", "", "Skipped assembly '{0}' because it was in an invalid format (see exception details below).", assembly);
                    continue;
                }
                catch (Exception ex)
                {
                    // other exceptions may be interesting though
                    throw ex;
                }
                finally
                {
                    sw.Stop();
                }
            }

            sw.Stop();
            Logging.GetLogging().LogFormat(LoggingMessageType.Trace, "Core", "", "Scanned {0} assemblies in {1} milliseconds (collected a total of {2} types).", assembliesToScan.Count(), sw.ElapsedMilliseconds, Exports.Count);

            IsInitialized = true;
        }

        /// <summary>
        /// Scans the given <paramref name="assembly"/>.
        /// </summary>
        /// <param name="assembly">The assembly to scan.</param>
        private void ScanAssembly(Assembly assembly)
        {
            if (assembly.GetCustomAttributes(typeof(ScanAssemblyAttribute), false).Length == 0)
            {
                Logging.GetLogging().LogFormat(LoggingMessageType.Debug, "Core", "", "Assembly '{0}' is ignored because it doesn't have the ScanAssembly attribute.", assembly);
                return;
            }

            int amount = 0;

            // process all types (even private ones)
            Type[] types = assembly.GetTypes();
            for (int i = 0; i < types.Length; i++)
            {
                Type t = types[i];

                var exports = t.GetCustomAttributes(typeof(ExportAttribute), false);
                if (exports.Length == 1)
                {
                    ExportAttribute export = exports[0] as ExportAttribute;
                    Exports.Add(new ExportedType(export, t));
                }

                amount++;
            }
        }

        /// <summary>
        /// Returns whether or not the given type implements the given interface.
        /// </summary>
        /// <param name="type"></param>
        /// <param name="interfaceType"></param>
        /// <returns></returns>
        public bool ImplementsInterface(Type type, Type interfaceType)
        {
            Type[] interfaces = type.GetInterfaces();
            for (int j = 0; j < interfaces.Length; j++)
            {
                Type iface = interfaces[j];

                if (iface == interfaceType)
                {
                    return true;
                }
            }
            return false;
        }

        /// <summary>
        /// Finds and creates the first exporter of a given interface.
        /// </summary>
        /// <typeparam name="T">The interface to create the first implementer of.</typeparam>
        /// <returns>The first implementer of a given interface.
        /// -or- <c>default{T}</c> if there were no implementers.</returns>
        public T Import<T>()
        {
            ExportedType[] exports = GetExports(typeof(T));
            if (exports.Length > 0)
            {
                return (T)Activator.CreateInstance(exports[0].Type);
            }

            return default(T);
        }

        /// <summary>
        /// Imports a named export.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="alias"></param>
        /// <returns></returns>
        public T Import<T>(string alias) 
        {
            ExportedType[] exports = GetExports(typeof(T));

            var e = exports.FirstOrDefault(t => t.Attribute.Alias == alias);
            if (e.Type != null)
            {
                return (T)Activator.CreateInstance(e.Type);
            }
            return default(T);
        }

        /// <summary>
        /// Returns an array with all exports that are known to the TypeManager.
        /// </summary>
        /// <returns>An array with all exports that are known to the TypeManager.</returns>
        public ExportedType[] GetExports()
        {
            return Exports.ToArray();
        }

        /// <summary>
        /// Returns an array with all exports that are known to the TypeManager.
        /// </summary>
        /// <param name="interfaceType">The type of the interface to find its implementers.</param>
        /// <returns>An array with all exports that are known to the TypeManager.</returns>
        public ExportedType[] GetExports(Type interfaceType)
        {
            List<ExportedType> result = new List<ExportedType>();
            foreach (ExportedType export in Exports)
            {
                if (ImplementsInterface(export.Type, interfaceType))
                {
                    result.Add(export);
                }
            }
            return result.ToArray();
        }

        /// <summary>
        /// Performs a reverse-lookup of the <see cref="ExportAttribute"/> attribute applied to the given type.
        /// </summary>
        /// <param name="exportedType">The type that has the <see cref="ExportAttribute"/> applied to it.</param>
        /// <returns></returns>
        public ExportAttribute GetExportAttribute(Type exportedType)
        {
            ExportAttribute[] attributes = (ExportAttribute[])exportedType.GetCustomAttributes(typeof(ExportAttribute), false);
            if (attributes.Length == 1)
            {
                return attributes[0];
            }
            return null;
        }

        #endregion
    }
}